import { BridgeDirection, createProviders, initProviders, ProvidersType } from "../core/ChainProviderFactory";
import { AccountManager, Accounts } from "../core/AccountManager";
import { sleep } from "../core/utils";
import { claim } from "../core/Claim";
import { createAccount } from "../core/CreateAccount";
import { Benchmark } from "./Benchmark";
import { StressArgs } from "./index";
import { BridgeConfig } from "../core/BridgeConfig";

export class Stress {
    accountManager: AccountManager;
    benchmark: Benchmark;
    providers: ProvidersType;
    bridgeConfig: BridgeConfig;
    initialDirection = BridgeDirection.XRP_TO_EVM;

    constructor(public args: StressArgs, public claimCommitAmount = 1) {
        this.bridgeConfig = args.config;
        this.providers = createProviders(this.initialDirection, this.bridgeConfig);
        this.accountManager = new AccountManager(this.providers);
        this.benchmark = new Benchmark(args.parallelClaims, args.claimIterations, args.parallelCreates, args.createIterations);
    }

    async run() {
        await initProviders(this.providers);
        const initialDirection = BridgeDirection.EVM_TO_XRP;
        const jobs: Promise<any>[] = [];
        await Promise.all([
            (async () => {
                for (let i = 0; i < this.args.parallelClaims; i++) {
                    jobs.push(this.claimJob(initialDirection, this.args.claimIterations));
                    await sleep(0.75);
                }
            })(),
            (async () => {
                for (let i = 0; i < this.args.parallelCreates; i++) {
                    jobs.push(this.createAccountJob(initialDirection, this.args.createIterations));
                    await sleep(0.75);
                }
            })(),
        ]);
        await Promise.all(jobs);
        this.benchmark.finish();
        await this.accountManager.refundAllUsedAccounts();
        this.benchmark.print();
    }

    private async periodicClaim(
        initialDirection: BridgeDirection,
        accounts: Accounts,
        currentLoop: number,
        maxLoops: number,
    ): Promise<void> {
        if (currentLoop === maxLoops) {
            return console.log(
                `periodicClaim [${currentLoop}/${maxLoops}] - Finished for ${accounts.source.address} & ${accounts.destination.address}`,
            );
        }

        console.log(
            `periodicClaim [${currentLoop}/${maxLoops}] - Executing claim for ${accounts.source.address} -> ${accounts.destination.address}`,
        );
        const claimId = await claim(accounts, this.claimCommitAmount);
        const metric = {
            startTime: new Date().getTime(),
            startBlock: await accounts.destination.provider.getBlock(),
            endTime: 0,
            endBlock: 0,
        };
        let isClaimed = await accounts.destination.provider.isClaimed(claimId, accounts.destination.address);
        while (!isClaimed) {
            await sleep(3);
            console.log(
                `periodicClaim [${currentLoop}/${maxLoops}] - Waiting attestation for ${accounts.destination.address} is not claimed yet`,
            );
            isClaimed = await accounts.destination.provider.isClaimed(claimId, accounts.destination.address);
        }
        metric.endTime = new Date().getTime();
        metric.endBlock = await accounts.destination.provider.getBlock();
        this.benchmark.addMetric({ type: "claim", direction: initialDirection, ...metric });

        const nextDirection = initialDirection === BridgeDirection.XRP_TO_EVM ? BridgeDirection.EVM_TO_XRP : BridgeDirection.XRP_TO_EVM;
        const nextAccounts: Accounts = {
            source: accounts.destination,
            destination: accounts.source,
        };
        return this.periodicClaim(nextDirection, nextAccounts, currentLoop + 1, maxLoops);
    }

    private async claimJob(initialDirection: BridgeDirection, iterations: number) {
        const accounts = this.accountManager.createAccounts(initialDirection);
        console.log(`Funding ${accounts.source.address} & ${accounts.destination.address}`);
        await this.accountManager.fundAccounts(
            accounts,
            this.claimCommitAmount + iterations * 2,
            (this.bridgeConfig.config.params.signatureReward + 1) * iterations,
        );

        let sourceBalance = 0,
            destinationBalance = 0;
        while (sourceBalance <= 0 || destinationBalance <= 0) {
            try {
                sourceBalance = await accounts.source.provider.getBalance(accounts.source.address);
                destinationBalance = await accounts.destination.provider.getBalance(accounts.destination.address);
            } catch (e) {
                console.log(`Accounts ${accounts.source.address} & ${accounts.destination.address} didn't get balance yet`);
                await sleep(3);
            }
        }
        try {
            await this.periodicClaim(initialDirection, accounts, 0, iterations);
        } catch (e) {
            console.log(`!ERROR! periodicClaim Failed for ${accounts.source.address} & ${accounts.destination.address} with error ${e}`);
        }
    }

    private async periodicCreateAccount(
        initialDirection: BridgeDirection,
        accounts: Accounts,
        currentLoop: number,
        maxLoops: number,
    ): Promise<void> {
        if (currentLoop === maxLoops) {
            await accounts.source.provider.refundAccount(accounts.source.pk);
            return console.log(
                `periodicCreateAccount [${currentLoop}/${maxLoops}] - Finished for ${accounts.source.address} & ${accounts.destination.address}`,
            );
        }

        console.log(
            `periodicCreateAccount [${currentLoop}/${maxLoops}] - Executing CreateAccountCommit for ${accounts.source.address} -> ${accounts.destination.address}`,
        );
        const sourceBalance = await accounts.source.provider.getBalance(accounts.source.address);
        let valueToSend = sourceBalance - this.bridgeConfig.config.params.signatureReward - 0.01;
        if (accounts.source.address.startsWith("r")) valueToSend -= 10;
        await createAccount(accounts, valueToSend);
        const metric = {
            startTime: new Date().getTime(),
            startBlock: await accounts.destination.provider.getBlock(),
            endTime: 0,
            endBlock: 0,
        };
        let isCreated = await accounts.destination.provider.isCreated(accounts.destination.address);
        while (!isCreated) {
            await sleep(3);
            console.log(
                `periodicCreateAccount [${currentLoop}/${maxLoops}] - Waiting attestation for ${accounts.destination.address} account is not created yet`,
            );
            isCreated = await accounts.destination.provider.isCreated(accounts.destination.address);
        }
        metric.endTime = new Date().getTime();
        metric.endBlock = await accounts.destination.provider.getBlock();
        this.benchmark.addMetric({ type: "claim", direction: initialDirection, ...metric });

        const nextDirection = initialDirection === BridgeDirection.XRP_TO_EVM ? BridgeDirection.EVM_TO_XRP : BridgeDirection.XRP_TO_EVM;

        const nextAccounts: Accounts = {
            source: accounts.destination,
            destination: this.accountManager.createAccounts(nextDirection).destination,
        };
        return this.periodicCreateAccount(nextDirection, nextAccounts, currentLoop + 1, maxLoops);
    }

    private async createAccountJob(initialDirection: BridgeDirection, iterations: number) {
        const accounts = this.accountManager.createAccounts(initialDirection);
        console.log(`Funding ${accounts.source.address}`);
        await this.accountManager.fundAccounts(accounts, this.bridgeConfig.config.params.minCreateAmount + iterations * 11, 0);
        let sourceBalance = 0;
        while (sourceBalance <= 0) {
            try {
                sourceBalance = await accounts.source.provider.getBalance(accounts.source.address);
            } catch (e) {
                console.log(`Account ${accounts.source.address} didn't get balance yet`);
                await sleep(3);
            }
        }
        await this.periodicCreateAccount(initialDirection, accounts, 0, iterations);
    }
}
